package broadcast;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException; 
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import common.Entity;
import common.Message;


public class Broadcast {
//	public static final int LISTENING_PORT = 40216;
//	public static final int SENDING_PORT = 40217;
//	public static final int REPLY_PORT = 40218;\
	public static final int NEW_VIEW = 0;
	public static final int OLD_VIEW = 1;
	
	private String componentId;
	private List<BroadcastListener> listeners;
	private Set<Entity> destinations;
	private Serializer serializer;
	private ReceiveMgr receiveMgr;
	private DatagramSocket socket;
	private int listenPort;
	private int sendPort;
	private int replyPort;
	private Map<Entity, Integer> entitySendSeq;
	private int viewNum;
	
	public Broadcast(BroadcastListener bl, Set<Entity> dests, int listen, int send, int reply) {
		listenPort = listen;
		sendPort = send;
		replyPort = reply;
		destinations = dests;
		listeners = new ArrayList<BroadcastListener>();
		serializer = new Serializer("qlwbEW9sRENi48RF", "AES/CFB/PKCS5Padding");
		entitySendSeq = new HashMap<Entity, Integer>();
		listeners.add(bl);
		try {
			socket = new DatagramSocket();
		} catch (SocketException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		viewNum = NEW_VIEW;
		receiveMgr = new ReceiveMgr(this);
		receiveMgr.start();
	}
	
	public void addObserver(BroadcastListener l) {
		listeners.add(l);
	}
	
	public synchronized boolean broadcast(Message m) {
		String toBeSent = new String(m.getData());
		if(toBeSent.contains("QUORUMTASK!0!1!")){
//			System.out.println("add client");
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		if(toBeSent.contains("QUORUMTASK!1!1!")){
//			System.out.println("delete client");
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		incrementSeqs();
		StringBuilder sb = new StringBuilder();
//		if(destinations.size() == 2) {
////			System.out.println(2);
//		}
//		Iterator<Entity> it = destinations.iterator();
//		while(it.hasNext()){
//			sb.append(it.next()).append(" ");
//		}
//		System.out.println(sb.toString());
		boolean result = sendMsg(destinations, m, true);
		viewNum = OLD_VIEW;
		return result;
	}
	
	private void incrementSeqs() {
		Entity[] dests = destinations.toArray(new Entity[destinations.size()]);
		for(int i = 0; i < dests.length; i ++	) {
//			if(dests[i] == null) {
//				continue;
//			}
			if(entitySendSeq.containsKey(dests[i])) {
				entitySendSeq.put(dests[i], entitySendSeq.get(dests[i]) + 1);
			} else {
				entitySendSeq.put(dests[i], 0);
			}
		}
	}
	
	boolean sendMsgNoIncrement(Message m) {
		return sendMsg(destinations, m, false);
	}
	
	synchronized boolean sendMsg(Set<Entity> dest, Message m, boolean setSeq) {
		Iterator<Entity> it = dest.iterator();
		while(it.hasNext()) {
			Entity receiver = it.next();
			if(receiver == null) {
				continue;
			}
			m.setDest(receiver);
			m.setViewNum(viewNum);
			if(setSeq) {
				m.setSeq(entitySendSeq.get(receiver));
			}
			System.out.println("Sending to "+receiver + " " + new String(m.getData()));
			byte[][] values = serializer.split(m);
			DatagramPacket[] packets = new DatagramPacket[values.length];
			for(int i = 0; i < values.length; i ++) {
				packets[i] = new DatagramPacket(values[i], values[i].length);
			}
			for(int i = 0; i < packets.length; i++) {
				try {
					packets[i].setAddress(InetAddress.getByName(receiver.getIp()));
					packets[i].setPort(listenPort);
					socket.send(packets[i]);
				} catch (UnknownHostException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
		return true;
	}
	
	boolean sendMsg(Message m) {
		return sendMsg(destinations, m, false);
	}
	
	// package visibility
	void deliver(Message m) {
		Entity src = m.getSource();
		System.out.println("Received from "+src + " " + new String(m.getData()));
		for(BroadcastListener bl : listeners) {
			bl.onDelivery(m);
		}
		// pass m up to appropriate queues.
	}

	/**
	 * @return the listenPort
	 */
	int getListenPort() {
		return listenPort;
	}

	/**
	 * @return the sendPort
	 */
	int getSendPort() {
		return sendPort;
	}

	/**
	 * @return the replyPort
	 */
	int getReplyPort() {
		return replyPort;
	}

	/**
	 * @return the serializer
	 */
	Serializer getSerializer() {
		return serializer;
	}	
	
	public synchronized void setDestinations(Set<Entity> dests){
		synchronized(destinations) {
			if(destinations.containsAll(dests) && dests.containsAll(destinations)) {
				return;
			}
			if(destinations == null) {
				destinations = dests;
				return;
			}
			Entity[] oldDests = destinations.toArray(new Entity[destinations.size()]);
			Entity[] newDests = dests.toArray(new Entity[dests.size()]);
			for(int i = 0; i < oldDests.length; i ++) {
				Entity oldDest = oldDests[i];
				if(!dests.contains(oldDest)) {
					destinations.remove(oldDest);
					entitySendSeq.remove(oldDest);
					receiveMgr.resetEntity(oldDest);
				}
			}
//			for(int i = 0; i < newDests.length; i ++) {
//				Entity newDest = newDests[i];
//				
//			}
			
			Iterator<Entity> newDestIter = dests.iterator();
			while(newDestIter.hasNext()) {
				Entity newDest = newDestIter.next();
				if(!destinations.contains(newDest)) {
					destinations.add(newDest);
				}
			}
		}
	}
//	
//	public synchronized void addDestination(Entity entity){
//		if(destinations.contains(entity)) {
//			return;
//		}
//		this.entitySendSeq.put(entity, 0);
//		this.destinations.add(entity);
//		setNewView();
//	}
//	public synchronized void deleteDestination(Entity entity){
//		this.entitySendSeq.remove(entity);
//		this.destinations.remove(entity);
//		setNewView();
//	}
	public synchronized void setComponentId(String s){
		componentId = s;
		receiveMgr.setComponentId(s);
	}
	
	public void resetEntity(Entity key) {
		entitySendSeq.put(key, 0);
		receiveMgr.resetEntity(key);
	}
}
